{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# GPU计算\n",
    "\n",
    "到目前为止，我们一直在使用CPU计算。对复杂的神经网络和大规模的数据来说，使用CPU来计算可能不够高效。在本节中，我们将介绍如何使用单块NVIDIA GPU来计算。首先，需要确保已经安装好了至少一块NVIDIA GPU。然后，下载CUDA并按照提示设置好相应的路径（可参考附录中[“使用AWS运行代码”](../chapter_appendix/aws.ipynb)一节）。这些准备工作都完成后，下面就可以通过`nvidia-smi`命令来查看显卡信息了。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "1"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Wed Feb 17 02:15:01 2021       \r\n",
      "+-----------------------------------------------------------------------------+\r\n",
      "| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |\r\n",
      "|-------------------------------+----------------------+----------------------+\r\n",
      "| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |\r\n",
      "| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |\r\n",
      "|===============================+======================+======================|\r\n",
      "|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |\r\n",
      "| N/A   51C    P0    53W / 300W |      0MiB / 16130MiB |      0%      Default |\r\n",
      "+-------------------------------+----------------------+----------------------+\r\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |\r\n",
      "| N/A   62C    P0    61W / 300W |      0MiB / 16130MiB |      0%      Default |\r\n",
      "+-------------------------------+----------------------+----------------------+\r\n",
      "|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |\r\n",
      "| N/A   41C    P0    53W / 300W |      0MiB / 16130MiB |      0%      Default |\r\n",
      "+-------------------------------+----------------------+----------------------+\r\n",
      "|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |\r\n",
      "| N/A   39C    P0    54W / 300W |      0MiB / 16130MiB |      4%      Default |\r\n",
      "+-------------------------------+----------------------+----------------------+\r\n",
      "                                                                               \r\n",
      "+-----------------------------------------------------------------------------+\r\n",
      "| Processes:                                                       GPU Memory |\r\n",
      "|  GPU       PID   Type   Process name                             Usage      |\r\n",
      "|=============================================================================|\r\n",
      "|  No running processes found                                                 |\r\n",
      "+-----------------------------------------------------------------------------+\r\n"
     ]
    }
   ],
   "source": [
    "!nvidia-smi  # 对Linux/macOS用户有效"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来，我们需要确认安装了MXNet的GPU版本。安装方法见[“获取和运行本书的代码”](../chapter_prerequisite/install.ipynb)一节。运行本节中的程序需要至少2块GPU。\n",
    "\n",
    "## 计算设备\n",
    "\n",
    "MXNet可以指定用来存储和计算的设备，如使用内存的CPU或者使用显存的GPU。默认情况下，MXNet会将数据创建在内存，然后利用CPU来计算。在MXNet中，`mx.cpu()`（或者在括号里填任意整数）表示所有的物理CPU和内存。这意味着，MXNet的计算会尽量使用所有的CPU核。但`mx.gpu()`只代表一块GPU和相应的显存。如果有多块GPU，我们用`mx.gpu(i)`来表示第$i$块GPU及相应的显存（$i$从0开始）且`mx.gpu(0)`和`mx.gpu()`等价。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(cpu(0), gpu(0), gpu(1))"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import mxnet as mx\n",
    "from mxnet import nd\n",
    "from mxnet.gluon import nn\n",
    "\n",
    "mx.cpu(), mx.gpu(), mx.gpu(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `NDArray`的GPU计算\n",
    "\n",
    "在默认情况下，`NDArray`存在内存上。因此，之前我们每次打印`NDArray`的时候都会看到`@cpu(0)`这个标识。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "4"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[1. 2. 3.]\n",
       "<NDArray 3 @cpu(0)>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x = nd.array([1, 2, 3])\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以通过`NDArray`的`context`属性来查看该`NDArray`所在的设备。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "cpu(0)"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x.context"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### GPU上的存储\n",
    "\n",
    "我们有多种方法将`NDArray`存储在显存上。例如，我们可以在创建`NDArray`的时候通过`ctx`参数指定存储设备。下面我们将`NDArray`变量`a`创建在`gpu(0)`上。注意，在打印`a`时，设备信息变成了`@gpu(0)`。创建在显存上的`NDArray`只消耗同一块显卡的显存。我们可以通过`nvidia-smi`命令查看显存的使用情况。通常，我们需要确保不创建超过显存上限的数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "5"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[1. 2. 3.]\n",
       "<NDArray 3 @gpu(0)>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a = nd.array([1, 2, 3], ctx=mx.gpu())\n",
    "a"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "假设至少有2块GPU，下面代码将会在`gpu(1)`上创建随机数组。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[[0.59119    0.313164   0.76352036]\n",
       " [0.9731786  0.35454726 0.11677533]]\n",
       "<NDArray 2x3 @gpu(1)>"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "B = nd.random.uniform(shape=(2, 3), ctx=mx.gpu(1))\n",
    "B"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "除了在创建时指定，我们也可以通过`copyto`函数和`as_in_context`函数在设备之间传输数据。下面我们将内存上的`NDArray`变量`x`复制到`gpu(0)`上。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "7"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[1. 2. 3.]\n",
       "<NDArray 3 @gpu(0)>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y = x.copyto(mx.gpu())\n",
    "y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[1. 2. 3.]\n",
       "<NDArray 3 @gpu(0)>"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "z = x.as_in_context(mx.gpu())\n",
    "z"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "需要区分的是，如果源变量和目标变量的`context`一致，`as_in_context`函数使目标变量和源变量共享源变量的内存或显存。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "8"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y.as_in_context(mx.gpu()) is y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "而`copyto`函数总是为目标变量开新的内存或显存。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y.copyto(mx.gpu()) is y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### GPU上的计算\n",
    "\n",
    "MXNet的计算会在数据的`context`属性所指定的设备上执行。为了使用GPU计算，我们只需要事先将数据存储在显存上。计算结果会自动保存在同一块显卡的显存上。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "9"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[ 20.085537 109.1963   445.2395  ]\n",
       "<NDArray 3 @gpu(0)>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "(z + 2).exp() * y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "注意，MXNet要求计算的所有输入数据都在内存或同一块显卡的显存上。这样设计的原因是CPU和不同的GPU之间的数据交互通常比较耗时。因此，MXNet希望用户确切地指明计算的输入数据都在内存或同一块显卡的显存上。例如，如果将内存上的`NDArray`变量`x`和显存上的`NDArray`变量`y`做运算，会出现错误信息。当我们打印`NDArray`或将`NDArray`转换成NumPy格式时，如果数据不在内存里，MXNet会将它先复制到内存，从而造成额外的传输开销。\n",
    "\n",
    "## Gluon的GPU计算\n",
    "\n",
    "同`NDArray`类似，Gluon的模型可以在初始化时通过`ctx`参数指定设备。下面的代码将模型参数初始化在显存上。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "12"
    }
   },
   "outputs": [],
   "source": [
    "net = nn.Sequential()\n",
    "net.add(nn.Dense(1))\n",
    "net.initialize(ctx=mx.gpu())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "当输入是显存上的`NDArray`时，Gluon会在同一块显卡的显存上计算结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "13"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[[0.0068339 ]\n",
       " [0.01366779]\n",
       " [0.02050169]]\n",
       "<NDArray 3x1 @gpu(0)>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面我们确认一下模型参数存储在同一块显卡的显存上。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "14"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\n",
       "[[0.0068339]]\n",
       "<NDArray 1x1 @gpu(0)>"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net[0].weight.data()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结\n",
    "\n",
    "* MXNet可以指定用来存储和计算的设备，如使用内存的CPU或者使用显存的GPU。在默认情况下，MXNet会将数据创建在内存，然后利用CPU来计算。\n",
    "* MXNet要求计算的所有输入数据都在内存或同一块显卡的显存上。\n",
    "\n",
    "## 练习\n",
    "\n",
    "* 试试大一点儿的计算任务，如大矩阵的乘法，看看使用CPU和GPU的速度区别。如果是计算量很小的任务呢？\n",
    "* GPU上应如何读写模型参数？\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "## 参考文献\n",
    "\n",
    "[1] CUDA下载地址。 https://developer.nvidia.com/cuda-downloads\n",
    "\n",
    "## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/988)\n",
    "\n",
    "![](../img/qr_use-gpu.svg)"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}